package	model.code;

import editor.NotImplementedException;
import event.SymbolChangeEvent;
import event.SymbolRenameEvent;
import event.SymbolTypeChangeEvent;
import lang.Language;
import lang.asm.DataTypeUtil;
import model.code.line.*;
import model.symbol.*;
import observer.ListObserver;
import observer.ObservedInt;
import observer.ObservedList;
import observer.ObservedString;

/**
 * This is an object to represent a global variable or array.
 *
 * @author Neil Dickson
 */
public class GlobalVariable extends CodeFileChunk implements Symbol, ScopeObserver, ListObserver<GlobalVariable.Piece>, SingleLine.LineObserver {
	private	ObservedList<Piece>		pieces;
	private	ObservedList<Rowable>	seeAlso;

	/**
	 * Holds the alignment if specified, or {@link CodeFileChunk#DEFAULT_ALIGNMENT_MARKER} if default
	 */
	private ObservedInt				alignment;
	/**
	 * Holds a string representing an expression that evaluates to the desired offset or empty if default
	 */
	private ValueLine				offset;
	/**
	 * Holds a string representing the type of scope that has access to this GlobalVariable.
	 * Possible values are contained in {@link #ACCESS_SCOPE_TYPES}.
	 * "" represents whatever the default is (currently global).
	 */
	private	ObservedString			accessScopeType;

	public GlobalVariable(String name, String description) {
		super(name, description);
		pieces = new ObservedList<Piece>();
		seeAlso = new ObservedList<Rowable>();
		pieces.addObserver(this);
		alignment = new ObservedInt(CodeFileChunk.DEFAULT_ALIGNMENT_MARKER);
		offset = new ValueLine(Line.EMPTY_CHAR_ARRAY,this,LOCATION_MODIFIERS,0);
		accessScopeType = new ObservedString();
	}

	public void setSeeAlso(String[] seeAlso) {
		this.seeAlso = new ObservedList<Rowable>(seeAlso.length);
		for (String string : seeAlso) {
			this.seeAlso.add(Rowable.SEE_ALSO_DUMMY.copy(this,this.seeAlso.size(),new String[]{string}));
		}
	}

	public ObservedList<Rowable> getSeeAlso() {
		return seeAlso;
	}

	public String toString() {
		return name.toString();
	}

	public void setType(String type) {
		throw new NotImplementedException();
	}

	public ObservedInt getAlignment() {
		return alignment;
	}

	public ObservedString getAccessScopeType() {
		return accessScopeType;
	}

	public ObservedList<Piece> getPieces() {
		return pieces;
	}

	public String getDefaultType() {
		if (pieces.isEmpty()) {
			// FIXME: This is language-dependent!!!
			return DataTypeUtil.getDataType("ptr",null,null,getMode()).toString().toUpperCase();
		}
		else {
			return pieces.get(0).getType().toString();
		}
	}

	public String getDefaultValue() {
		// FIXME: This is language-dependent!!!
		return (DataTypeUtil.isPrimitiveType(getDefaultType())) ? "?" : "<>";
	}

	/**
	 * This adds this GlobalVariable as an observer of the global {@link Scope}.
	 */
	public void nowHaveGlobalScope() {
		for (Piece piece : pieces) {
			piece.nowHaveGlobalScope();
		}
		getGlobalScope().addObserver(this);
	}

	public void symbolAdded(Scope scope, Symbol symbol) {
		if (symbol instanceof DataType) {
			for (Piece piece : pieces) {
				// TODO: Do this a more correct way, like what's done in symbolChanged(), only for symbol removal.
				if ((piece.getType().getSymbolAt(0)==null || piece.getType().getNumReferences()>1) && piece.getType().toString().contains(symbol.getName().toString())) {
					piece.getType().reparse();
				}
			}
		}
		for (Piece piece : pieces) {
			piece.getInitialValue().reparse();
		}
		// TODO: Notify global scope of any change to the type if needed.
	}

	public void symbolRemoved(Scope scope, Symbol symbol) {
		if (symbol instanceof DataType) {
			for (Piece piece : pieces) {
				// TODO: Do this a more correct way, like what's done in symbolChanged(), only for symbol removal.
				if (piece.getType().toString().contains(symbol.getName().toString())) {
					piece.getType().reparse();
				}
			}
		}
		for (Piece piece : pieces) {
			piece.getInitialValue().reparse();
		}
		// TODO: Notify global scope of any change to the type if needed.
	}

	public void symbolChanged(Scope scope, SymbolChangeEvent e) {
		Symbol symbol = e.getSymbol();
		if (symbol instanceof DataType && e instanceof SymbolRenameEvent) {
			SymbolRenameEvent e1 = (SymbolRenameEvent)e;
			String name = e1.getNewName();
			for (Piece piece : pieces) {
				TypeLine type = piece.getType();
				for (int i=0;i<type.getNumReferences();++i) {
					if (type.getReferences()[i]==symbol) {
						piece.getType().replace(type.getReferenceIndices()[i],(i+1<type.getNumReferences())?type.getReferenceIndices()[i+1]:type.length(),name);
					}
					else if (type.getReferences()[i]==null && type.toString(type.getReferenceIndices()[i],(i+1<type.getNumReferences())?type.getReferenceIndices()[i+1]:type.length()).equals(name)) {
						type.getReferences()[i] = symbol;
					}
				}
			}
		}
		for (Piece piece : pieces) {
			piece.getInitialValue().reparse(e instanceof SymbolRenameEvent);
		}
		// TODO: Notify global scope of any change to the type if needed.
	}

	public GlobalVariable copy() {
		GlobalVariable that = new GlobalVariable(getName().toString(),getDescription().toString());
		that.pieces.ensureCapacity(pieces.size());
		for (Piece piece : pieces) {
			that.pieces.add(piece.copy(that,that.pieces.size()));
		}
		that.seeAlso.ensureCapacity(seeAlso.size());
		for (Rowable sa : seeAlso) {
			that.seeAlso.add(sa.copy(that,that.seeAlso.size()));
		}
		that.alignment = alignment.copy();
		that.offset = offset.copy(that);
		that.accessScopeType = accessScopeType.copy();
		return that;
	}

	public void listElementAdded(ObservedList<Piece> s, int index) {
		if (index==0 && s==pieces) {
			if (pieces.size()>1) {
				pieces.get(1).getType().removeObserver(this);
			}
			pieces.get(0).getType().addObserver(this);
		}
	}

	public void listElementRemoved(ObservedList<Piece> s, int index, Piece element) {
		if (index==0 && s==pieces) {
			element.getType().removeObserver(this);
			if (pieces.size()>0) {
				pieces.get(0).getType().addObserver(this);
			}
		}
	}

	public void listElementSet(ObservedList<Piece> s, int index, Piece oldElement) {
		if (index==0 && s==pieces) {
			oldElement.getType().removeObserver(this);
			pieces.get(0).getType().addObserver(this);
		}
	}

	public void listElementMoved(ObservedList<Piece> s, int from, int to) {
		if (s==pieces && from!=to) {
			if (from==0) {
				pieces.get(to).getType().removeObserver(this);
				pieces.get(0).getType().addObserver(this);
			}
			else if (to==0) {
				pieces.get(1).getType().removeObserver(this);
				pieces.get(0).getType().addObserver(this);
			}
		}
	}

	/**
	 * This is notified on changes to the type field of the first {@link Piece} of this {@link GlobalVariable},
	 * so that the global {@link Scope} can be notified.
	 * @param s the {@link ObservedString} that holds the type of the first {@link Piece}
	 * @param oldValue the previous value of that type
	 */
	public void valueChanged(SingleLine s, String oldValue) {
		Scope globalScope = getGlobalScope();
		if (globalScope!=null) {
			globalScope.symbolChanged(new SymbolTypeChangeEvent(this,oldValue,s.toString()));
		}
	}

	public ValueLine getOffset() {
		return offset;
	}

	public LineList getLineList(int locType, int locIndex) {
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		ObservedList<? extends Rowable> table = null;
		if (locType==LOCATION_VAR_PIECES) {
			table = pieces;
		}
		else if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		else if (locType==LOCATION_MODIFIERS) {
			return offset;
		}
		else {
			return super.getLineList(locType, locIndex);
		}
		if (table!=null && table.size()>row && table.get(row).getStrings().length>col) {
			return table.get(row).getStrings()[col];
		}
		return null;
	}
	public LineList getNextLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return description;
		}
		if (locType==LOCATION_DESCRIPTION) {
			return getLineList(LOCATION_MODIFIERS,0);
		}
		if (locType==LOCATION_MODIFIERS) {
			if (pieces.size()>0) {
				return getLineList(LOCATION_VAR_PIECES,0);
			}
			if (seeAlso.size()>0) {
				return getLineList(LOCATION_SEE_ALSO,0);
			}
			return null;
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_VAR_PIECES) {
			table = pieces;
		}
		if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		if (table!=null) {
			if (col+1<table.get(row).getSize()) {
				return getLineList(locType,locIndex+1);
			}
			if (row+1<table.size()) {
				return getLineList(locType,(row+1)<<16);
			}
			if (locType==LOCATION_VAR_PIECES && seeAlso.size()>0) {
				return getLineList(LOCATION_SEE_ALSO,0);
			}
		}
		return null;
	}
	public LineList getPrevLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return null;
		}
		if (locType==LOCATION_DESCRIPTION) {
			return name;
		}
		if (locType==LOCATION_MODIFIERS) {
			return description;
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_VAR_PIECES) {
			table = pieces;
		}
		if (locType==LOCATION_SEE_ALSO) {
			table = seeAlso;
		}
		if (table!=null) {
			if (col-1>=0) {
				return getLineList(locType,locIndex-1);
			}
			if (row-1>=0) {
				return getLineList(locType,(row-1)<<16);
			}
			if (locType==LOCATION_SEE_ALSO && pieces.size()>0) {
				return pieces.getLast().getStrings()[pieces.getLast().getSize()-1];
			}
			return getLineList(LOCATION_MODIFIERS,0);
		}
		return null;
	}

	public LineList getLastLineList() {
		if (seeAlso.size()>0) {
			return seeAlso.getLast().getStrings()[seeAlso.getLast().getSize()-1];
		}
		if (pieces.size()>0) {
			return pieces.getLast().getStrings()[pieces.getLast().getSize()-1];
		}
		return getLineList(LOCATION_MODIFIERS,0);
	}

	public int getSize() {
		int total = 0;
		for (Piece piece : pieces) {
			total += piece.getTotalSize();
		}
		return total;
	}

	public static class Piece extends Rowable implements SingleLine.LineObserver {
		/**
		 * This is because Java can't do virtual static functions
		 */
		public static final Piece	DUMMY = new Piece("","",null,0);

		private static final int TYPE_INDEX		= 0;
		private static final int VALUE_INDEX	= 1;
		public static final int NUM_STRINGS		= 2;

		public Piece(String type, String initialValue, CodeFileChunk parent, int row) {
			super(NUM_STRINGS,parent,CodeFileChunk.LOCATION_VAR_PIECES,row);
			strings[TYPE_INDEX] = new TypeLine(type.toCharArray(),parent,CodeFileChunk.LOCATION_VAR_PIECES,(row<<16)+TYPE_INDEX);
			strings[VALUE_INDEX] = new ValueLine(initialValue.toCharArray(),parent,CodeFileChunk.LOCATION_VAR_PIECES,(row<<16)+VALUE_INDEX);
			getInitialValue().addObserver(this);
		}
		public TypeLine getType() {
			return (TypeLine)strings[TYPE_INDEX];
		}
		public ValueLine getInitialValue() {
			return (ValueLine)strings[VALUE_INDEX];
		}
		public Piece copy(CodeFileChunk parent, int row) {
			return new Piece(getType().toString(),getInitialValue().toString(),parent,row);
		}

		public void nowHaveGlobalScope() {
			getType().reparse();
			getInitialValue().reparse();
		}

		public void valueChanged(SingleLine line, String oldValue) {
			getInitialValue().reparse();
		}
		/**
		 * Determines the total size of the variable piece by parsing its value and looking at its type.
		 * @return total size in bytes of the Piece, or 0 if erroneous
		 */
		public int getTotalSize() {
			if (getInitialValue().getParent()!=null) {
				GlobalVariable globalVariable = (GlobalVariable)getInitialValue().getParent();
				Language language = globalVariable.getLanguage();
				if (language!=null) {
					DataType dataType = DataTypeUtil.getDataType(getType().toString(),language.getScope(),globalVariable.getGlobalScope(),globalVariable.getMode());
					if (dataType!=null) {
						Object sizeOrError = language.getParser().evaluateArray(getInitialValue(),dataType,null,null,0,getInitialValue().getNumReferences());
						if (sizeOrError!=null && sizeOrError instanceof Integer) {
							return (Integer)sizeOrError;
						}
					}
				}
			}
			return 0;
		}
	}
}
